消息处理

在接收到消息后,根据消息类型进行不同的处理。参考示例如下

// 接收消息
const handleWsMessage = (data) => {
    if (Array.isArray(data) && data.length) return;
    if (data.type === "start") {
      createWsMsg(data);
    } else if (data.type === "stream") {
      updateCurrentMessage(
        {
          chat_id: data.chat_id,
          message: data.message,
          thought: data.intermediate_steps,
        },
        false
      );
    } else if (["end", "end_cover"].includes(data.type)) {
      updateCurrentMessage(
        {
          ...data,
          end: true,
          thought: data.intermediate_steps || "",
          messageId: data.message_id,
          noAccess: false,
          liked: 0,
        },
        data.type === "end_cover"
      );
    } else if (data.type === "close") {
      // 本次会话结束
    }
  };

// type=start时
const runLogsTypes = ['tool', 'flow', 'knowledge']
createWsMsg(data) {
  set((state) => {
    let newChat = cloneDeep(state.messages);
    let message = ''
    if (runLogsTypes.includes(data.category)) {
      message = JSON.parse(data.message)
    } else if (data.category === 'node') {
      message = data.message
    }

    newChat.push({
      isSend: false,
      message: message,
      chatKey: '',
      thought: data.intermediate_steps || '',
      category: data.category || '',
      files: [],
      end: false,
      user_name: '',
      extra: data.extra,
      type: data.category === 'node' ? 'start' : '',
      knowledge_source: null,
    })
    return { messages: newChat }
  })
}

// 更新消息
const runLogsTypes = ['tool', 'flow', 'knowledge']
updateCurrentMessage(wsdata, cover = false) {
  const messages = get().messages
  const isRunLog = runLogsTypes.includes(wsdata.category);
  // run log类型存在嵌套情况,使用 extra 匹配 currentMessage; 否则取最近
  const currentMessageIndex = isRunLog ?
      messages.findLastIndex((msg) => msg.extra === wsdata.extra)
      : messages.findLastIndex((msg) => !runLogsTypes.includes(msg.category))
  const currentMessage = messages[currentMessageIndex]
  let message = null
  if (isRunLog) {
    message = JSON.parse(wsdata.message)
  } else if (wsdata.category === 'node') {
    message = wsdata.message
  } else {
    message = currentMessage.message + wsdata.message
  }
  const newCurrentMessage = {
    ...currentMessage,
    ...wsdata,
    id: isRunLog ? wsdata.extra : wsdata.messageId, // 每条消息必唯一
    message: message,
    thought: currentMessage.thought + (wsdata.thought ? `${wsdata.thought}\n` : ''),
    files: wsdata.files || [],
    category: wsdata.category || '',
    source: wsdata.source,
    type: wsdata.type || '',
  }
  messages[currentMessageIndex] = newCurrentMessage

  // 会话特殊处理
  if (!isRunLog) {
    // start - end 之间没有内容删除load
    if (newCurrentMessage.end && !(newCurrentMessage.files?.length || newCurrentMessage.thought || newCurrentMessage.message)) {
      messages.pop()
    }
    // 删除重复消息
    const prevMessage = messages[currentMessageIndex - 1];
    if ((prevMessage && prevMessage.message === newCurrentMessage.message && prevMessage.thought === newCurrentMessage.thought)
        || cover) {
      const removedMsg = messages.pop()
      // 使用最后一条的信息作为准确信息
      Object.keys(prevMessage).forEach((key) => {
          prevMessage[key] = removedMsg[key]
      })
    }
  }

  set((state) => ({
      messages: [...messages],
  }))
}

普通消息展示

{messages.map((msg) => {
  let type = "llm";
  if (msg.isSend) {
    type = "user";
  } else if (msg.category === "divider") {
    type = "separator";
  } else if (msg.category === "knowledge_source" && msg.type === "end") {
    type = "knowledge_source";
  } else if (["tool", "flow", "knowledge"].includes(msg.category)) {
    type = "runLog";
  } else if (msg.thought) {
    type = "system";
  } else if (msg.category === 'node') {
    type = "node";
  }

  switch (type) {
    // 用户发送的消息
    case "user":
      return <MessageUser key={msg.id} useName={useName} data={msg} />;
    // 大模型返回的消息
    case "llm":
      return (
        <Message
          key={msg.id}
          data={msg}
        />
      );
    default:
      return (
        <div className="mt-2 rounded-md border p-2 text-sm" key={msg.id}>
          未知消息类型
        </div>
      );
    }
  })
}

特定类型消息展示-小程序

示例消息数据

{
  "is_bot":true,
  "message":"已为你找到相关服务",
  "type":"end_cover",
  "category":"answer",
  "intermediate_steps":null,
  "files":[],
  "user_id":1,
  "message_id":"c48a56ae71cd460093eacf34b3f08b6d",
  "source":0,
  "sender":null,
  "receiver":null,
  "liked":0,
  "extra":"\"{}\"",
  "flow_id":"94062040-ffc6-4cb9-b202-39a31d958358",
  "chat_id":"b792930b25c8ae5f5af6db6b1958c377",
  "knowledge_source":[],
  "answer_files":[],
  "is_microApp":{
    "id":"MicroApp-d6480",
    "pageList":[
      {
        "key": "0",
        "code": "<!DOCTYPE html>前端代码-1</html>",
        "label": "首页",
        "pageId": "0"
      },
      {
        "key": "1",
        "code": "<!DOCTYPE html>前端代码-2</html>",
        "label": "成功页",
        "pageId": "success"
      }
    ]
  }
}

该消息仅在type=end_cover且category=answer时,判断is_microApp字段是否存在,如果存在则表示可以输出渲染小程序。通过监听message事件将会获取到相应的交互数据,下面以React+iframe为示例的部分主要代码片段:

useEffect(() => {
  // 添加对 message 事件的监听器
  window.addEventListener('message', handleIframe);

  // 清除监听器
  return () => {
    window.removeEventListener('message', handleIframe);
  };
}, []);

const handleIframe = async (event) => {
  console.log(event, '会话页面---小程序接收数据 event')
  if (!event.data) {
    return
  }
  // 处理接收到的消息
  let appData = event.data
  if (typeof event.data === 'string') {
    appData = JSON.parse(event.data);
  }
  const { height, type, formData } = appData
  switch (type) {
    case 'iframe-height': // 该类型为用于控制小程序高度
      // 为保证不同屏幕的兼容性,增加2px作为冗余
      iframeRef.current.style.height = height + 2 + 'px';
      break;
    case 'form-data': // 该类型为小程序相关按钮事件产生的数据
      const newData = JSON.parse(formData);
      // 根据pageId跳转到相应的页面
      if (newData.appId === iframeRef.current.id) {
        const page = data.is_microApp.pageList.find(item => item.pageId === newData.pageId)
        if (page && iframeRef.current) {
          setIframeLoading(true)
          iframeRef.current.contentWindow?.document.open();
          iframeRef.current.contentWindow?.document.write(page.code || '');
          iframeRef.current.contentWindow?.document.close();
          iframeRef.current.onload = () => {
            setIframeLoading(false)
            iframeRef.current.contentWindow?.postMessage(formData)
          };
        }
      }
      break;
    case 'iframe-url': // 该类型用于判断是否在会话页面内嵌入iframe展示指定页面
      const urlData = JSON.parse(formData);
      setChatLayoutIframeUrl(urlData.url)
      break
  }
}

// iframe部分
<div style={{ height: iframeLoading ? 48 : '100%' }}>
  <iframe
    ref={iframeRef}
    sandbox="allow-scripts allow-same-origin allow-modals allow-forms"
    title="preview"
    srcDoc=""
    width="100%"
    height="100%"
    id={data.is_microApp.id}
    className="app-iframe"
  />
</div>

由于小程序的相关操作可能导致高度存在改变的情况,因此根据需要可以进行重置iframe高度的相关操作。

const resizeIframe = () => {
  const innerDoc = iframeRef.current.contentDocument || iframeRef.current.contentWindow.document;
  iframeRef.current.style.height = innerDoc.body.scrollHeight + 'px';
  setIframeLoading(false)
}